/*
 * @(#)FPSTimer.java	1.0 Apr 27, 2008
 *
 *	The MIT License
 *
 *	Copyright (c) 2008 Malachi de AElfweald <malachid@gmail.com>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 */
package org.eoti.swing.util;

import static org.eoti.util.TimeUtil.Nanosecond;
import static org.eoti.util.TimeUtil.Second;
import org.eoti.swing.ActiveRenderer;

public class FPSTimer
extends Thread
{
    // these are NOT final static so that we can override by extension
    protected int MAX_NODELAYS_BEFORE_YIELD = 16;
    protected int MAX_FRAME_SKIPS = 5;
    protected int MAX_ARRAY_SIZE = 10;

    protected ActiveRenderer renderer;
    protected int reqFPS;
    protected long periodNs;
    protected boolean running;

    // for stats
    protected long oneSecond = Nanosecond.convertFrom(1, Second);
    protected long startTime;
    protected long totalTime = 0L;
    //protected int timeSpentSec = 0;
    protected long prevStatsTime;
    protected double fps[];
    protected double ups[];
    protected double averageFPS = 0.0;
    protected double averageUPS = 0.0;
    protected long frameCount = 0;
    protected long statsCount = 0;
    protected long framesSkipped = 0L;
    protected long totalSkipped = 0L;
    protected long statsInterval = 0L;
    protected boolean printStats = false;

    public FPSTimer(ActiveRenderer renderer, int reqFPS)
    {
        super("FPSTimer");
        this.renderer = renderer;
        this.reqFPS = reqFPS;
        this.periodNs = oneSecond / reqFPS;

        fps = new double[MAX_ARRAY_SIZE];
        ups = new double[MAX_ARRAY_SIZE];
        for(int i=0; i<MAX_ARRAY_SIZE; i++)
        {
            fps[i] = 0.0;
            ups[i] = 0.0;
        }
    }

    public void printStats(boolean enabled)
    {printStats = enabled;}

    public void run()
    {
        /**
         * This design was altered from the textbook at
         * http://fivedots.coe.psu.ac.th/~ad/jg/index.html
         */

        long beforeTime, afterTime, timeDiff;

        long sleepTime = 0L;
        long overSleepTime = 0L;
        long excess = 0L;
        int noDelays = 0;

        startTime = System.nanoTime();
        prevStatsTime = startTime;
        beforeTime = startTime;
        running = true;

        while(running)
        {
            renderer.activeUpdate();
            renderer.activeRender();

            afterTime = System.nanoTime();
            timeDiff = afterTime - beforeTime;
            sleepTime = (periodNs - timeDiff) - overSleepTime;

            if(sleepTime > 0) 			// Didn't sleep long enough
            {
                Nanosecond.sleep(sleepTime);
                overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
            }else{						// Slept too long
                excess -= sleepTime;
                overSleepTime = 0L;
                if(++noDelays >= MAX_NODELAYS_BEFORE_YIELD)
                {
                    Thread.yield();		// let someone else try to process
                    noDelays = 0;
                }
            }

            beforeTime = System.nanoTime();
            /**
             * rendering is taking too long. do some extra activeUpdate() to get
             * the Updates/sec closer to requested Frames/sec
             */
            int skips = 0;
            while((excess > periodNs) && (skips < MAX_FRAME_SKIPS))
            {
                excess -= periodNs;
                renderer.activeUpdate();
                skips++;
            }

            framesSkipped += skips;
            storeStats();
        }

        // finishOff();//cleanup
    }

    public void end()
    {
        running = false;
    }

    protected void storeStats()
    {
        frameCount++;
        statsInterval += periodNs;
        if(statsInterval < oneSecond)
            return;

        long timeNow = System.nanoTime();
        //timeSpentSec = (int)Second.convertFrom((timeNow-startTime), Nanosecond);
        long sincePrev = timeNow - prevStatsTime;
        totalTime += sincePrev;
        double timingError = ((double)(sincePrev - statsInterval) / statsInterval) * 100.0;
        totalSkipped += framesSkipped;

        double actualFPS = 0.0;
        double actualUPS = 0.0;
        if(totalTime > 0)
        {
            long time = (totalTime / oneSecond);
            actualFPS = (double)(	frameCount						/time);
            actualUPS = (double)(	(frameCount + totalSkipped)		/time);
        }

        fps[ (int)(statsCount%MAX_ARRAY_SIZE) ] = actualFPS;
        ups[ (int)(statsCount%MAX_ARRAY_SIZE) ] = actualUPS;
        statsCount++;

        double totalFPS = 0.0;
        double totalUPS = 0.0;
        for(int i=0; i<MAX_ARRAY_SIZE; i++)
        {
            totalFPS += fps[i];
            totalUPS += ups[i];
        }

        averageFPS = totalFPS / statsCount;
        averageUPS = totalUPS / statsCount;

        if(printStats)
        {
            System.out.format(
                "%01.2f/%01.2f sec %03.1f%% err; %05d frames (%03d/%03d skip); %03.2f(%03.2f) fps; %03.2f(%03.2f) ups\n",
                (double)(statsInterval / oneSecond),
                (double)(sincePrev / oneSecond),
                (double)timingError,
                frameCount,
                framesSkipped,
                totalSkipped,
                actualFPS,
                averageFPS,
                actualUPS,
                averageUPS
            );
        }

        framesSkipped = 0;
        prevStatsTime = timeNow;
        statsInterval = 0L;
    }

    public double getAverageFPS(){return averageFPS;}
    public double getAverageUPS(){return averageUPS;}
    public long getRunningTime(){return totalTime;}
    public long getFrameCount(){return frameCount;}
    public long getFramesSkipped(){return totalSkipped;}
}